上节课我们讨论了 Webpack 的最新版本 Webpack 5 所带来的提效新功能。思考题是 Webpack 5 中的持久化缓存究竟会影响哪些构建环节呢?
通过对 compiler.cache.hook.get 的追踪不难发现:持久化缓存一共影响下面这些环节与内置的插件:
正是通过这样多环节的缓存读写控制,才打造出 Webpack 5 高效的持久化缓存功能。
在之前的课程里我们详细分解了 Webpack 构建工具的效率优化方案,这节课我们来聊一聊今年比较火的另一种构建工具思路:无包构建(No-Bundle/Unbundle)。
什么是无包构建呢?这是一个与基于模块化打包的构建方案相对的概念。
在“ 第 9 课时|构建总览:前端构建工具的演进”中谈到过,目前主流的构建工具,例如 Webpack、Rollup 等都是基于一个或多个入口点模块,通过依赖分析将有依赖关系的模块打包到一起,最后形成少数几个产物代码包,因此这些工具也被称为打包工具。只不过,这些工具的构建过程除了打包外,还包括了模块编译和代码优化等,因此称为打包式构建工具或许更恰当。
而无包构建是指这样一类构建方式:在构建时只需处理模块的编译而无须打包,把模块间的**依赖关系完全交给浏览器来处理。**浏览器会加载入口模块,分析依赖后,再通过网络请求加载被依赖的模块。通过这样的方式简化构建时的处理过程,提升构建效率。
这种通过浏览器原生的模块进行解析的方式又称为 Native-ESM(Native ES Module)。下面我们就通过一个简单示例来展示这种基于浏览器的模块加载过程(16_nobundle/simple-esm),如下面的代码和图片所示:
//./src/index.html
...
<script type="module" src="./modules/foo.js"></script>
...
//.src/modules/foo.js
import { bar } from './bar.js'
import { appendHTML } from './common.js'
...
import('https://cdn.jsdelivr.net/npm/[email protected]/slice.js').then((module) => {...})
从示例中可以看到,在没有任何构建工具处理的情况下,在页面中引入带有 type="module" 属性的 script,浏览器就会在加载入口模块时依次加载了所有被依赖的模块。下面我们就来深入了解一下这种基于浏览器加载 JS 模块的技术的细节。
从 caniuse 网站中可以看到,目前大部分主流的浏览器都已支持 JavaScript modules 这一特性,如下图所示:
[图片来源:https://caniuse.com/es6-module]
我们来总结这种加载方式的注意点。
从上面的技术细节中我们会发现,对于一个普通的项目而言,要使用这种加载方案仍然有几个主要问题:
下面,我们分析 Vite 和 Snowpack 这两个有代表性的构建工具是如何解决上面的问题的。
Vite 是 Vue 框架的作者尤雨溪最新推出的基于 Native-ESM 的 Web 构建工具。它在开发环境下基于 Native-ESM 处理构建过程,只编译不打包,在生产环境下则基于 Rollup 打包。我们还是先通过 Vite 的官方示例来观察它的使用效果,如下面的代码和图片所示(示例代码参见 example-vite):
npm init vite-app example-vite
cd example-vite
npm install
npm run dev
可以看到,运行示例代码后,在浏览器中只引入了 src/main.js 这一个入口模块,但是在网络面板中却依次加载了若干依赖模块,包括外部模块 vue 和 css。依赖图如下:
可以看到,经过 Vite 处理后,浏览器中加载的模块与源代码中导入的模块相比发生了变化,这些变化包括对外部依赖包的处理,对 vue 文件的处理,对 css 文件的处理等。下面我们就来逐个分析其中的变化。
对 HTML 文件的预处理
当启动 Vite 时,会通过 serverPluginHtml.ts 注入 /vite/client 运行时的依赖模块,该模块用于处理热更新,以及提供更新 CSS 的方法 updateStyle。
对外部依赖包的解析
首先是对不带路径前缀的外部依赖包(也称为Bare Modules)的解析,例如上图中在示例源代码中导入了 'vue' 模块,但是在浏览器的网络请求中变为了请求 /@module/vue。
这个解析过程在 Vite 中主要通过三个文件来处理:
对 Vue文件的解析
对 Vue 文件的解析是通过 serverPluginVue.ts
处理的,分离出 Vue 代码中的 script/template/style 代码片段,并分别转换为 JS 模块,然后将
template/style 模块的 import写到script 模块代码的头部。因此在浏览器访问时,一个 Vue
源代码文件会分裂为 2~3 的关联请求(例如上面的 /src/App.vue 和
/src/App.vue?type=template,如果 App.vue 中包含 <style>
则会产生第 3
个请求 /src/App.vue?type=style)。
对 CSS 文件的解析
对 CSS 文件的解析是通过 serverPluginCSS.ts 处理的,解析过程主要是将 CSS 文件的内容转换为下面的 JS 代码模块,其中的 updateStyle 由注入 HTML 中的 /vite/client 模块提供,如下面的代码所示:
import { updateStyle } from "/vite/client"
const css = "..."
updateStyle("\"...\"", css) // id, cssContent
export default css
以上就是示例代码中主要文件类型的基本解析逻辑,可以看到,Vite 正是通过这些解析器来解决不同类型文件以 JS 模块的方式在浏览器中加载的问题。在 Vite 源码中还包含了其他更多文件类型的解析器,例如 JSON、TS、SASS 等,这里就不一一列举了,感兴趣的话,你可以进一步查阅官方文档。
除了提供这些解析器的能力外,Vite 还提供了其他便捷的构建功能,大致整理如下:
Vite 的使用限制如下:
Snowpack 是另一个比较知名的无包构建工具,从整体功能来说和上述 Vite工具提供的功能大致相同,主要差异点在 Snowpack 在生产环境下默认使用无包构建而非打包模式(可以通过引入打包插件例如 @snowpack/plugin-webpack 来实现打包模式),而 Vite 仅在开发模式下使用。示例代码参见 example-snow。下面我们简单整理下两者的异同。
两者都支持各种代码转换加载器、热更新、环境变量(需要安装 dotenv 插件)、服务代理、HTTPS 与 HTTP/2 等。
通过上面的 Vite 等无包构建工具的功能介绍可以发现,同 Webpack 等主流打包构建工具相比,无包构建流程的优缺点都十分明显。
无包构建的最大优势在于构建速度快,尤其是启动服务的初次构建速度要比目前主流的打包构建工具要快很多,原因如下:
这节课我们主要讨论了今年比较热门的无包构建。
无包构建产生的基础是浏览器对 JS 模块加载的支持,这样才可能把构建过程中分析模块依赖关系并打包的过程变为在浏览器中逐个加载引用的模块。但是这种加载模块的方式在实际项目应用场景下还存在一些阻碍,于是有了无包构建工具。
在这些工具里,我们主要介绍了 Vite 和 Snowpack,希望通过介绍他们的开发模式的基本工作流程和差异点,让你对这类工具的功能特点有一个基本的了解。
今天的课后思考题是,为什么 Vite/Snowpack 这样的无包构建工具要比 Webpack 这样的打包构建工具速度更快呢?
随着这节课的结束,构建优化模块也就告一段落了。下节课开始我们将进入部署优化模块。